/**
 * Copyright (c) 2006-2015, JGraph Ltd
 * Copyright (c) 2006-2015, Gaudenz Alder
 */
/**
 * Class: mxConstraintHandler
 *
 * Handles constraints on connection targets. This class is in charge of
 * showing fixed points when the mouse is over a vertex and handles constraints
 * to establish new connections.
 *
 * Constructor: mxConstraintHandler
 *
 * Constructs an new constraint handler.
 *
 * Parameters:
 *
 * graph - Reference to the enclosing <mxGraph>.
 * factoryMethod - Optional function to create the edge. The function takes
 * the source and target <mxCell> as the first and second argument and
 * returns the <mxCell> that represents the new edge.
 */
function mxConstraintHandler(graph)
{
    this.graph = graph;

    // Adds a graph model listener to update the current focus on changes
    this.resetHandler = mxUtils.bind(this, function(sender, evt)
    {
        if (this.currentFocus != null && this.graph.view.getState(this.currentFocus.cell) == null)
        {
            this.reset();
        }
        else
        {
            this.redraw();
        }
    });

    this.graph.model.addListener(mxEvent.CHANGE, this.resetHandler);
    this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.resetHandler);
    this.graph.view.addListener(mxEvent.TRANSLATE, this.resetHandler);
    this.graph.view.addListener(mxEvent.SCALE, this.resetHandler);
    this.graph.addListener(mxEvent.ROOT, this.resetHandler);
};

/**
 * Variable: pointImage
 *
 * <mxImage> to be used as the image for fixed connection points.
 */
mxConstraintHandler.prototype.pointImage = new mxImage(mxClient.imageBasePath + '/point.gif', 5, 5);

/**
 * Variable: graph
 *
 * Reference to the enclosing <mxGraph>.
 */
mxConstraintHandler.prototype.graph = null;

/**
 * Variable: enabled
 *
 * Specifies if events are handled. Default is true.
 */
mxConstraintHandler.prototype.enabled = true;

/**
 * Variable: highlightColor
 *
 * Specifies the color for the highlight. Default is <mxConstants.DEFAULT_VALID_COLOR>.
 */
mxConstraintHandler.prototype.highlightColor = mxConstants.DEFAULT_VALID_COLOR;

/**
 * Function: isEnabled
 *
 * Returns true if events are handled. This implementation
 * returns <enabled>.
 */
mxConstraintHandler.prototype.isEnabled = function()
{
    return this.enabled;
};

/**
 * Function: setEnabled
 *
 * Enables or disables event handling. This implementation
 * updates <enabled>.
 *
 * Parameters:
 *
 * enabled - Boolean that specifies the new enabled state.
 */
mxConstraintHandler.prototype.setEnabled = function(enabled)
{
    this.enabled = enabled;
};

/**
 * Function: reset
 *
 * Resets the state of this handler.
 */
mxConstraintHandler.prototype.reset = function()
{
    if (this.focusIcons != null)
    {
        for (var i = 0; i < this.focusIcons.length; i++)
        {
            this.focusIcons[i].destroy();
        }

        this.focusIcons = null;
    }

    if (this.focusHighlight != null)
    {
        this.focusHighlight.destroy();
        this.focusHighlight = null;
    }

    this.currentConstraint = null;
    this.currentFocusArea = null;
    this.currentPoint = null;
    this.currentFocus = null;
    this.focusPoints = null;
};

/**
 * Function: getTolerance
 *
 * Returns the tolerance to be used for intersecting connection points. This
 * implementation returns <mxGraph.tolerance>.
 *
 * Parameters:
 *
 * me - <mxMouseEvent> whose tolerance should be returned.
 */
mxConstraintHandler.prototype.getTolerance = function(me)
{
    return this.graph.getTolerance();
};

/**
 * Function: getImageForConstraint
 *
 * Returns the tolerance to be used for intersecting connection points.
 */
mxConstraintHandler.prototype.getImageForConstraint = function(state, constraint, point)
{
    return this.pointImage;
};

/**
 * Function: isEventIgnored
 *
 * Returns true if the given <mxMouseEvent> should be ignored in <update>. This
 * implementation always returns false.
 */
mxConstraintHandler.prototype.isEventIgnored = function(me, source)
{
    return false;
};

/**
 * Function: isStateIgnored
 *
 * Returns true if the given state should be ignored. This always returns false.
 */
mxConstraintHandler.prototype.isStateIgnored = function(state, source)
{
    return false;
};

/**
 * Function: destroyIcons
 *
 * Destroys the <focusIcons> if they exist.
 */
mxConstraintHandler.prototype.destroyIcons = function()
{
    if (this.focusIcons != null)
    {
        for (var i = 0; i < this.focusIcons.length; i++)
        {
            this.focusIcons[i].destroy();
        }

        this.focusIcons = null;
        this.focusPoints = null;
    }
};

/**
 * Function: destroyFocusHighlight
 *
 * Destroys the <focusHighlight> if one exists.
 */
mxConstraintHandler.prototype.destroyFocusHighlight = function()
{
    if (this.focusHighlight != null)
    {
        this.focusHighlight.destroy();
        this.focusHighlight = null;
    }
};

/**
 * Function: isKeepFocusEvent
 *
 * Returns true if the current focused state should not be changed for the given event.
 * This returns true if shift and alt are pressed.
 */
mxConstraintHandler.prototype.isKeepFocusEvent = function(me)
{
    return mxEvent.isShiftDown(me.getEvent());
};

/**
 * Function: getCellForEvent
 *
 * Returns the cell for the given event.
 */
mxConstraintHandler.prototype.getCellForEvent = function(me, point)
{
    var cell = me.getCell();

    // Gets cell under actual point if different from event location
    if (cell == null && point != null && (me.getGraphX() != point.x || me.getGraphY() != point.y))
    {
        cell = this.graph.getCellAt(point.x, point.y);
    }

    // Uses connectable parent vertex if one exists
    if (cell != null && !this.graph.isCellConnectable(cell))
    {
        var parent = this.graph.getModel().getParent(cell);

        if (this.graph.getModel().isVertex(parent) && this.graph.isCellConnectable(parent))
        {
            cell = parent;
        }
    }

    return (this.graph.isCellLocked(cell)) ? null : cell;
};

/**
 * Function: update
 *
 * Updates the state of this handler based on the given <mxMouseEvent>.
 * Source is a boolean indicating if the cell is a source or target.
 */
mxConstraintHandler.prototype.update = function(me, source, existingEdge, point)
{
    if (this.isEnabled() && !this.isEventIgnored(me))
    {
        // Lazy installation of mouseleave handler
        if (this.mouseleaveHandler == null && this.graph.container != null)
        {
            this.mouseleaveHandler = mxUtils.bind(this, function()
            {
                this.reset();
            });

            mxEvent.addListener(this.graph.container, 'mouseleave', this.resetHandler);
        }

        var tol = this.getTolerance(me);
        var x = (point != null) ? point.x : me.getGraphX();
        var y = (point != null) ? point.y : me.getGraphY();
        var grid = new mxRectangle(x - tol, y - tol, 2 * tol, 2 * tol);
        var mouse = new mxRectangle(me.getGraphX() - tol, me.getGraphY() - tol, 2 * tol, 2 * tol);
        var state = this.graph.view.getState(this.getCellForEvent(me, point));

        // Keeps focus icons visible while over vertex bounds and no other cell under mouse or shift is pressed
        if (!this.isKeepFocusEvent(me) && (this.currentFocusArea == null || this.currentFocus == null ||
            (state != null) || !this.graph.getModel().isVertex(this.currentFocus.cell) ||
            !mxUtils.intersects(this.currentFocusArea, mouse)) && (state != this.currentFocus))
        {
            this.currentFocusArea = null;
            this.currentFocus = null;
            this.setFocus(me, state, source);
        }

        this.currentConstraint = null;
        this.currentPoint = null;
        var minDistSq = null;

        if (this.focusIcons != null && this.constraints != null &&
            (state == null || this.currentFocus == state))
        {
            var cx = mouse.getCenterX();
            var cy = mouse.getCenterY();

            for (var i = 0; i < this.focusIcons.length; i++)
            {
                var dx = cx - this.focusIcons[i].bounds.getCenterX();
                var dy = cy - this.focusIcons[i].bounds.getCenterY();
                var tmp = dx * dx + dy * dy;

                if ((this.intersects(this.focusIcons[i], mouse, source, existingEdge) || (point != null &&
                    this.intersects(this.focusIcons[i], grid, source, existingEdge))) &&
                    (minDistSq == null || tmp < minDistSq))
                {
                    this.currentConstraint = this.constraints[i];
                    this.currentPoint = this.focusPoints[i];
                    minDistSq = tmp;

                    var tmp = this.focusIcons[i].bounds.clone();
                    tmp.grow(mxConstants.HIGHLIGHT_SIZE + 1);
                    tmp.width -= 1;
                    tmp.height -= 1;

                    if (this.focusHighlight == null)
                    {
                        var hl = this.createHighlightShape();
                        hl.dialect = (this.graph.dialect == mxConstants.DIALECT_SVG) ?
                            mxConstants.DIALECT_SVG : mxConstants.DIALECT_VML;
                        hl.pointerEvents = false;

                        hl.init(this.graph.getView().getOverlayPane());
                        this.focusHighlight = hl;

                        var getState = mxUtils.bind(this, function()
                        {
                            return (this.currentFocus != null) ? this.currentFocus : state;
                        });

                        mxEvent.redirectMouseEvents(hl.node, this.graph, getState);
                    }

                    this.focusHighlight.bounds = tmp;
                    this.focusHighlight.redraw();
                }
            }
        }

        if (this.currentConstraint == null)
        {
            this.destroyFocusHighlight();
        }
    }
    else
    {
        this.currentConstraint = null;
        this.currentFocus = null;
        this.currentPoint = null;
    }
};

/**
 * Function: redraw
 *
 * Transfers the focus to the given state as a source or target terminal. If
 * the handler is not enabled then the outline is painted, but the constraints
 * are ignored.
 */
mxConstraintHandler.prototype.redraw = function()
{
    if (this.currentFocus != null && this.constraints != null && this.focusIcons != null)
    {
        var state = this.graph.view.getState(this.currentFocus.cell);
        this.currentFocus = state;
        this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);

        for (var i = 0; i < this.constraints.length; i++)
        {
            var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
            var img = this.getImageForConstraint(state, this.constraints[i], cp);

            var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
                Math.round(cp.y - img.height / 2), img.width, img.height);
            this.focusIcons[i].bounds = bounds;
            this.focusIcons[i].redraw();
            this.currentFocusArea.add(this.focusIcons[i].bounds);
            this.focusPoints[i] = cp;
        }
    }
};

/**
 * Function: setFocus
 *
 * Transfers the focus to the given state as a source or target terminal. If
 * the handler is not enabled then the outline is painted, but the constraints
 * are ignored.
 */
mxConstraintHandler.prototype.setFocus = function(me, state, source)
{
    this.constraints = (state != null && !this.isStateIgnored(state, source) &&
        this.graph.isCellConnectable(state.cell)) ? ((this.isEnabled()) ?
        (this.graph.getAllConnectionConstraints(state, source) || []) : []) : null;

    // Only uses cells which have constraints
    if (this.constraints != null)
    {
        this.currentFocus = state;
        this.currentFocusArea = new mxRectangle(state.x, state.y, state.width, state.height);

        if (this.focusIcons != null)
        {
            for (var i = 0; i < this.focusIcons.length; i++)
            {
                this.focusIcons[i].destroy();
            }

            this.focusIcons = null;
            this.focusPoints = null;
        }

        this.focusPoints = [];
        this.focusIcons = [];

        for (var i = 0; i < this.constraints.length; i++)
        {
            var cp = this.graph.getConnectionPoint(state, this.constraints[i]);
            var img = this.getImageForConstraint(state, this.constraints[i], cp);

            var src = img.src;
            var bounds = new mxRectangle(Math.round(cp.x - img.width / 2),
                Math.round(cp.y - img.height / 2), img.width, img.height);
            var icon = new mxImageShape(bounds, src);
            icon.dialect = (this.graph.dialect != mxConstants.DIALECT_SVG) ?
                mxConstants.DIALECT_MIXEDHTML : mxConstants.DIALECT_SVG;
            icon.preserveImageAspect = false;
            icon.init(this.graph.getView().getDecoratorPane());

            // Fixes lost event tracking for images in quirks / IE8 standards
            if (mxClient.IS_QUIRKS || document.documentMode == 8)
            {
                mxEvent.addListener(icon.node, 'dragstart', function(evt)
                {
                    mxEvent.consume(evt);

                    return false;
                });
            }

            // Move the icon behind all other overlays
            if (icon.node.previousSibling != null)
            {
                icon.node.parentNode.insertBefore(icon.node, icon.node.parentNode.firstChild);
            }

            var getState = mxUtils.bind(this, function()
            {
                return (this.currentFocus != null) ? this.currentFocus : state;
            });

            icon.redraw();

            mxEvent.redirectMouseEvents(icon.node, this.graph, getState);
            this.currentFocusArea.add(icon.bounds);
            this.focusIcons.push(icon);
            this.focusPoints.push(cp);
        }

        this.currentFocusArea.grow(this.getTolerance(me));
    }
    else
    {
        this.destroyIcons();
        this.destroyFocusHighlight();
    }
};

/**
 * Function: createHighlightShape
 *
 * Create the shape used to paint the highlight.
 *
 * Returns true if the given icon intersects the given point.
 */
mxConstraintHandler.prototype.createHighlightShape = function()
{
    var hl = new mxRectangleShape(null, this.highlightColor, this.highlightColor, mxConstants.HIGHLIGHT_STROKEWIDTH);
    hl.opacity = mxConstants.HIGHLIGHT_OPACITY;

    return hl;
};

/**
 * Function: intersects
 *
 * Returns true if the given icon intersects the given rectangle.
 */
mxConstraintHandler.prototype.intersects = function(icon, mouse, source, existingEdge)
{
    return mxUtils.intersects(icon.bounds, mouse);
};

/**
 * Function: destroy
 *
 * Destroy this handler.
 */
mxConstraintHandler.prototype.destroy = function()
{
    this.reset();

    if (this.resetHandler != null)
    {
        this.graph.model.removeListener(this.resetHandler);
        this.graph.view.removeListener(this.resetHandler);
        this.graph.removeListener(this.resetHandler);
        this.resetHandler = null;
    }

    if (this.mouseleaveHandler != null && this.graph.container != null)
    {
        mxEvent.removeListener(this.graph.container, 'mouseleave', this.mouseleaveHandler);
        this.mouseleaveHandler = null;
    }
};
